/*
 * Copyright (C) 2012  Romain DUBOIS
 * 
 * This file is part of NeMoS - Network and Modular Software
 * 
 * NeMoS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * NeMoS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with NeMoS.  If not, see <http://www.gnu.org/licenses/>.
 */
package nemos.launch.internal;

import java.io.Serializable;
import java.text.ParseException;
import java.util.Arrays;

import nemos.launch.progress.IRecords;

/**
 * RecordsImpl : The records implementation.
 * 
 * @author Romain DUBOIS
 */
public class RecordsImpl implements Serializable, IRecords {

    /** The empty records serialization result */
    static final String EMPTY_RECORDS = ""; //$NON-NLS-1$

    /** The levels separator */
    private static final String LEVELS_SEPARATOR = ";"; //$NON-NLS-1$

    /** Serial version */
    private static final long serialVersionUID = 1L;
    /** The record values separator */
    private static final String VALUES_SEPARATOR = ","; //$NON-NLS-1$

    /** The level data observed for the current launch */
    private LevelData[] mCurrentData;
    /** The default level start time */
    private final long mDefaultTime;
    /** The first launch flag */
    private boolean mFirstLaunch;
    /** The previous level data */
    private LevelData[] mRecordedData;

    /**
     * Constructor
     * 
     * @param pInitTime
     *            The initialization time
     * @param pDefaultTime
     *            The default level start time
     * @param pLaunchLevel
     *            The framework launch level
     * @param pTargetLevel
     *            The target start level
     */
    public RecordsImpl(final long pInitTime, final long pDefaultTime, final int pLaunchLevel,
            final int pTargetLevel) {
        super();

        mFirstLaunch = true;
        mDefaultTime = pDefaultTime;
        mRecordedData = new LevelData[pLaunchLevel + 1];
        if (mRecordedData.length > 1) {
            mRecordedData[1] = new LevelData(0, pInitTime);
        }
        for (int i = 2; i < mRecordedData.length; i++) {
            mRecordedData[i] = new LevelData(0, pDefaultTime);
        }
        final long lTotalTime = pInitTime + (pDefaultTime * (pTargetLevel - pLaunchLevel));
        mRecordedData[0] = new LevelData(0, lTotalTime);
        mCurrentData = new LevelData[pTargetLevel + 1];
    }

    /**
     * Constructor
     * 
     * @param pInitTime
     *            The initialization time
     * @param pDefaultTime
     *            The default level start time
     * @param pLaunchLevel
     *            The framework launch level
     * @param pTargetLevel
     *            The target start level
     * @param pSerializedForm
     *            The serialized records
     * @throws ParseException
     *             If the string is invalid
     */
    public RecordsImpl(final long pInitTime, final long pDefaultTime, final int pLaunchLevel,
            final int pTargetLevel, final String pSerializedForm) throws ParseException {
        this(pInitTime, pDefaultTime, pLaunchLevel, pTargetLevel);

        if (!pSerializedForm.isEmpty()) {
            int lPos = 0;
            try {
                final String[] lLevels = pSerializedForm.split(LEVELS_SEPARATOR);
                mRecordedData = new LevelData[lLevels.length + 1];
                long lTotalTime = 0;
                int lTotalBundlesNb = 0;

                // Fill arrays with previous data
                int lLevel = 1;
                for (final String lLevelDesc : lLevels) {
                    final String[] lData = lLevelDesc.split(VALUES_SEPARATOR);

                    // First : started bundles
                    final int lBundlesNb = Integer.parseInt(lData[0]);
                    lPos += lData[0].length() + VALUES_SEPARATOR.length();

                    // Then : time spent
                    final long lTime = Long.parseLong(lData[1]);
                    lPos += lData[1].length() + LEVELS_SEPARATOR.length();

                    // Assemble and update data
                    mRecordedData[lLevel] = new LevelData(lBundlesNb, lTime);
                    lTotalTime += lTime;
                    lTotalBundlesNb += lBundlesNb;
                    lLevel++;
                }

                // Total data
                mRecordedData[0] = new LevelData(lTotalBundlesNb, lTotalTime);

                mFirstLaunch = false;
            } catch (final NumberFormatException e) {
                throw new ParseException(e.getMessage(), lPos);
            } catch (final IndexOutOfBoundsException e) {
                throw new ParseException(e.getMessage(), lPos);
            }
        }
    }

    /**
     * Add a level data for the current launch.
     * 
     * @param pCurrentLevel
     *            The level to add
     * @param pStartedBundles
     *            The number started bundles in the ended level
     * @param pSpentTimeInLevel
     *            The time spent to launch the level
     */
    public void addLevel(final int pCurrentLevel, final int pStartedBundles,
            final long pSpentTimeInLevel) {
        mCurrentData[pCurrentLevel + 1] = new LevelData(pStartedBundles, pSpentTimeInLevel);
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.launch.progress.IRecords#getBundleNumber(int)
     */
    @Override
    public int getBundleNumber(final int pLevel) {
        if ((pLevel + 1) < mRecordedData.length) {
            return mRecordedData[pLevel + 1].getStartedBundles();
        }
        return 0;
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.launch.progress.IRecords#getTime(int)
     */
    @Override
    public long getTime(final int pLevel) {
        if ((pLevel) < mRecordedData.length) {
            return mRecordedData[pLevel].getTimeSpent();
        }
        return mDefaultTime;
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.launch.progress.IRecords#isFirstLaunch()
     */
    @Override
    public boolean isFirstLaunch() {
        return mFirstLaunch;
    }

    /**
     * Compile the results.
     */
    void compile() {

        // Ensure current array length
        if (mCurrentData.length < mRecordedData.length) {
            mCurrentData = Arrays.copyOf(mCurrentData, mRecordedData.length);
        }

        // Compile into current data array
        for (int lLevel = 0; lLevel < mCurrentData.length - 1; lLevel++) {
            final LevelData lCurrent = getNotNull(mCurrentData, lLevel);
            final LevelData lRecorded = getNotNull(mRecordedData, lLevel);

            final int lNewNb = (lCurrent.getStartedBundles() + 2 * lRecorded.getStartedBundles()) / 3;
            final long lNewTime = (lCurrent.getTimeSpent() + 2L * lRecorded.getTimeSpent()) / 3L;

            mCurrentData[lLevel + 1] = new LevelData(lNewNb, lNewTime);
        }
    }

    /**
     * Serialize the records.
     * 
     * @return The serialized form of the current records.
     */
    String serialize() {
        final StringBuilder lBuilder = new StringBuilder();

        // Ignore total data (which does not exists anyway)
        for (int lLevel = 1; lLevel < mCurrentData.length; lLevel++) {
            if (lLevel > 1) {
                lBuilder.append(LEVELS_SEPARATOR);
            }

            lBuilder.append(mCurrentData[lLevel].getStartedBundles());
            lBuilder.append(VALUES_SEPARATOR);
            lBuilder.append(mCurrentData[lLevel].getTimeSpent());
        }
        return lBuilder.toString();
    }

    /**
     * Get a not-null level data.
     * 
     * @param pLevels
     *            The level data
     * @param pLevel
     *            The level
     * @return The default level data or the specified one
     */
    private LevelData getNotNull(final LevelData[] pLevels, final int pLevel) {
        LevelData lLevel = null;
        if ((pLevel + 1) < pLevels.length) {
            lLevel = pLevels[pLevel + 1];
        }

        if (lLevel == null) {
            return new LevelData(0, mDefaultTime);
        }
        return lLevel;
    }
}
